#!/bin/bash
# /usr/lib/check_mk_agent/local/300/check_portainer

set -u

CONFIG="/etc/check_mk/portainer.conf"

OK=0
WARN=1
CRIT=2
UNKNOWN=3

#
# ---------------------------------------------------------------------------
# Configuration
# ---------------------------------------------------------------------------
#

if [[ ! -r "$CONFIG" ]]; then
    printf '%s\n' "${UNKNOWN} \"Portainer\" - Configuration file ${CONFIG} missing or unreadable"
    exit 0
fi

# shellcheck disable=SC1090
source "$CONFIG"

PORTAINER_URL="${PORTAINER_URL:-https://127.0.0.1:9443}"
PORTAINER_TOKEN="${PORTAINER_TOKEN:-}"
PORTAINER_INSECURE="${PORTAINER_INSECURE:-yes}"
ALERT_STOPPED_CONTAINERS="${ALERT_STOPPED_CONTAINERS:-no}"
CHECK_UPDATES="${CHECK_UPDATES:-yes}"
PORTAINER_RELEASE_API="${PORTAINER_RELEASE_API:-https://api.github.com/repos/portainer/portainer/releases/latest}"

#
# ---------------------------------------------------------------------------
# Requirements
# ---------------------------------------------------------------------------
#

if ! command -v curl >/dev/null 2>&1; then
    printf '%s\n' "${UNKNOWN} \"Portainer\" - curl is not installed"
    exit 0
fi

if ! command -v jq >/dev/null 2>&1; then
    printf '%s\n' "${UNKNOWN} \"Portainer\" - jq is not installed"
    exit 0
fi

if ! command -v sort >/dev/null 2>&1; then
    printf '%s\n' "${UNKNOWN} \"Portainer\" - sort is not installed"
    exit 0
fi

if [[ -z "$PORTAINER_TOKEN" ]]; then
    printf '%s\n' "${UNKNOWN} \"Portainer\" - PORTAINER_TOKEN is not configured"
    exit 0
fi

#
# ---------------------------------------------------------------------------
# curl configuration
# ---------------------------------------------------------------------------
#

CURL_OPTS=(
    --silent
    --show-error
    --connect-timeout 3
    --max-time 10
)

if [[ "$PORTAINER_INSECURE" == "yes" ]]; then
    CURL_OPTS+=(--insecure)
fi

AUTH_HEADER="X-API-Key: ${PORTAINER_TOKEN}"

#
# ---------------------------------------------------------------------------
# Portainer health / version
# ---------------------------------------------------------------------------
#

STATUS_RESPONSE="$(
    curl \
        "${CURL_OPTS[@]}" \
        "${PORTAINER_URL%/}/api/status" \
        2>/dev/null
)"

CURL_RC=$?

if [[ $CURL_RC -ne 0 || -z "$STATUS_RESPONSE" ]]; then
    printf '%s\n' "${CRIT} \"Portainer\" - API unavailable at ${PORTAINER_URL}"
    exit 0
fi

if ! printf '%s' "$STATUS_RESPONSE" | jq -e . >/dev/null 2>&1; then
    printf '%s\n' "${UNKNOWN} \"Portainer\" - Status API returned invalid JSON"
    exit 0
fi

VERSION="$(
    printf '%s' "$STATUS_RESPONSE" |
        jq -r '.Version // .version // empty'
)"

if [[ -n "$VERSION" ]]; then
    printf '%s\n' "${OK} \"Portainer\" - API reachable, version ${VERSION}"
else
    printf '%s\n' "${OK} \"Portainer\" - API reachable"
fi

#
# ---------------------------------------------------------------------------
# Portainer update check
# ---------------------------------------------------------------------------
#

if [[ "$CHECK_UPDATES" == "yes" ]]; then

    if [[ -z "$VERSION" ]]; then
        printf '%s\n' \
            "${UNKNOWN} \"Portainer Update\" - Installed Portainer version could not be determined"

    else

        RELEASE_RESPONSE="$(
            curl \
                "${CURL_OPTS[@]}" \
                -H "Accept: application/vnd.github+json" \
                -H "User-Agent: checkmk-portainer-local-check" \
                "$PORTAINER_RELEASE_API" \
                2>/dev/null
        )"

        CURL_RC=$?

        if [[ $CURL_RC -ne 0 || -z "$RELEASE_RESPONSE" ]]; then
            printf '%s\n' \
                "${UNKNOWN} \"Portainer Update\" - Unable to retrieve latest release information"

        elif ! printf '%s' "$RELEASE_RESPONSE" | jq -e . >/dev/null 2>&1; then
            printf '%s\n' \
                "${UNKNOWN} \"Portainer Update\" - Release API returned invalid JSON"

        else

            LATEST_VERSION="$(
                printf '%s' "$RELEASE_RESPONSE" |
                    jq -r '.tag_name // empty'
            )"

            INSTALLED_VERSION="${VERSION#v}"
            LATEST_VERSION="${LATEST_VERSION#v}"

            if [[ -z "$LATEST_VERSION" ]]; then

                printf '%s\n' \
                    "${UNKNOWN} \"Portainer Update\" - Latest release version could not be determined"

            else

                #
                # Strip known suffixes before numeric comparison.
                #
                # Examples:
                #   2.36.0
                #   2.36.0-sts
                #   2.36.0-lts
                #

                INSTALLED_COMPARE="${INSTALLED_VERSION%%-*}"
                LATEST_COMPARE="${LATEST_VERSION%%-*}"

                if [[ ! "$INSTALLED_COMPARE" =~ ^[0-9]+([.][0-9]+)*$ ]]; then
                    printf '%s\n' \
                        "${UNKNOWN} \"Portainer Update\" - Unable to parse installed version ${INSTALLED_VERSION}"

                elif [[ ! "$LATEST_COMPARE" =~ ^[0-9]+([.][0-9]+)*$ ]]; then
                    printf '%s\n' \
                        "${UNKNOWN} \"Portainer Update\" - Unable to parse latest version ${LATEST_VERSION}"

                else

                    NEWEST="$(
                        printf '%s\n%s\n' \
                            "$INSTALLED_COMPARE" \
                            "$LATEST_COMPARE" |
                            sort -V |
                            tail -n 1
                    )"

                    if [[ "$INSTALLED_COMPARE" == "$LATEST_COMPARE" ]]; then

                        printf '%s\n' \
                            "${OK} \"Portainer Update\" - Current: installed ${INSTALLED_VERSION}, latest ${LATEST_VERSION}"

                    elif [[ "$NEWEST" == "$LATEST_COMPARE" ]]; then

                        printf '%s\n' \
                            "${WARN} \"Portainer Update\" - Update available: installed ${INSTALLED_VERSION}, latest ${LATEST_VERSION}"

                    else

                        printf '%s\n' \
                            "${OK} \"Portainer Update\" - Installed ${INSTALLED_VERSION} is newer than latest stable ${LATEST_VERSION}"

                    fi
                fi
            fi
        fi
    fi
fi

#
# ---------------------------------------------------------------------------
# Endpoints
# ---------------------------------------------------------------------------
#

ENDPOINTS_RESPONSE="$(
    curl \
        "${CURL_OPTS[@]}" \
        -H "$AUTH_HEADER" \
        "${PORTAINER_URL%/}/api/endpoints" \
        2>/dev/null
)"

CURL_RC=$?

if [[ $CURL_RC -ne 0 || -z "$ENDPOINTS_RESPONSE" ]]; then
    printf '%s\n' "${CRIT} \"Portainer Endpoints\" - Unable to retrieve endpoints"
    exit 0
fi

if ! printf '%s' "$ENDPOINTS_RESPONSE" | jq -e 'type == "array"' >/dev/null 2>&1; then
    printf '%s\n' "${UNKNOWN} \"Portainer Endpoints\" - Endpoint API returned unexpected data"
    exit 0
fi

ENDPOINT_COUNT="$(
    printf '%s' "$ENDPOINTS_RESPONSE" |
        jq 'length'
)"

UP_ENDPOINTS=0
DOWN_ENDPOINTS=0

while IFS= read -r ENDPOINT; do

    ENDPOINT_ID="$(
        printf '%s' "$ENDPOINT" |
            jq -r '.Id // .ID // empty'
    )"

    ENDPOINT_NAME="$(
        printf '%s' "$ENDPOINT" |
            jq -r '.Name // "Unknown"'
    )"

    ENDPOINT_STATUS="$(
        printf '%s' "$ENDPOINT" |
            jq -r '.Status // 0'
    )"

    ENDPOINT_NAME="${ENDPOINT_NAME//\"/\'}"

    #
    # Portainer endpoint states:
    #   1 = UP
    #   2 = DOWN
    #

    if [[ "$ENDPOINT_STATUS" == "1" ]]; then

        UP_ENDPOINTS=$((UP_ENDPOINTS + 1))

        printf '%s\n' \
            "${OK} \"Portainer Endpoint ${ENDPOINT_NAME}\" - Endpoint is UP"

    else

        DOWN_ENDPOINTS=$((DOWN_ENDPOINTS + 1))

        printf '%s\n' \
            "${CRIT} \"Portainer Endpoint ${ENDPOINT_NAME}\" - Endpoint is DOWN, status ${ENDPOINT_STATUS}"

    fi

    #
    # -----------------------------------------------------------------------
    # Containers for this endpoint
    # -----------------------------------------------------------------------
    #

    if [[ -z "$ENDPOINT_ID" ]]; then
        printf '%s\n' \
            "${UNKNOWN} \"Docker Containers ${ENDPOINT_NAME}\" - Endpoint ID missing"
        continue
    fi

    CONTAINERS_RESPONSE="$(
        curl \
            "${CURL_OPTS[@]}" \
            -H "$AUTH_HEADER" \
            "${PORTAINER_URL%/}/api/endpoints/${ENDPOINT_ID}/docker/containers/json?all=1" \
            2>/dev/null
    )"

    CURL_RC=$?

    if [[ $CURL_RC -ne 0 || -z "$CONTAINERS_RESPONSE" ]]; then
        printf '%s\n' \
            "${UNKNOWN} \"Docker Containers ${ENDPOINT_NAME}\" - Unable to retrieve containers"
        continue
    fi

    if ! printf '%s' "$CONTAINERS_RESPONSE" | jq -e 'type == "array"' >/dev/null 2>&1; then
        printf '%s\n' \
            "${UNKNOWN} \"Docker Containers ${ENDPOINT_NAME}\" - Container API returned unexpected data"
        continue
    fi

    TOTAL_CONTAINERS="$(
        printf '%s' "$CONTAINERS_RESPONSE" |
            jq 'length'
    )"

    RUNNING_CONTAINERS="$(
        printf '%s' "$CONTAINERS_RESPONSE" |
            jq '[.[] | select(.State == "running")] | length'
    )"

    STOPPED_CONTAINERS="$(
        printf '%s' "$CONTAINERS_RESPONSE" |
            jq '[.[] | select(.State != "running")] | length'
    )"

    UNHEALTHY_CONTAINERS="$(
        printf '%s' "$CONTAINERS_RESPONSE" |
            jq '
                [
                    .[]
                    | select(
                        ((.Status // "") | test("\\(unhealthy\\)"))
                    )
                ]
                | length
            '
    )"

    PROBLEM_LIST="$(
        printf '%s' "$CONTAINERS_RESPONSE" |
            jq -r '
                .[]
                | select(
                    (.State != "running")
                    or
                    ((.Status // "") | test("\\(unhealthy\\)"))
                )
                | (
                    (
                        (.Names[0] // .Id // "unknown")
                        | sub("^/"; "")
                    )
                    + "="
                    + (.Status // .State // "unknown")
                )
            ' |
            paste -sd ';' -
    )"

    STATE=$OK

    if [[ "$UNHEALTHY_CONTAINERS" -gt 0 ]]; then
        STATE=$CRIT

    elif [[ "$ALERT_STOPPED_CONTAINERS" == "yes" && "$STOPPED_CONTAINERS" -gt 0 ]]; then
        STATE=$WARN
    fi

    PERFDATA="total=${TOTAL_CONTAINERS};;;0|running=${RUNNING_CONTAINERS};;;0|stopped=${STOPPED_CONTAINERS};;;0|unhealthy=${UNHEALTHY_CONTAINERS};;;0"

    DETAILS="${RUNNING_CONTAINERS}/${TOTAL_CONTAINERS} running"

    if [[ "$UNHEALTHY_CONTAINERS" -gt 0 ]]; then
        DETAILS="${DETAILS}, ${UNHEALTHY_CONTAINERS} unhealthy"
    fi

    if [[ "$STOPPED_CONTAINERS" -gt 0 ]]; then
        DETAILS="${DETAILS}, ${STOPPED_CONTAINERS} stopped"
    fi

    if [[ -n "$PROBLEM_LIST" ]]; then
        DETAILS="${DETAILS}: ${PROBLEM_LIST}"
    fi

    printf '%s\n' \
        "${STATE} \"Docker Containers ${ENDPOINT_NAME}\" ${PERFDATA} ${DETAILS}"

done < <(
    printf '%s' "$ENDPOINTS_RESPONSE" |
        jq -c '.[]'
)

#
# ---------------------------------------------------------------------------
# Endpoint summary
# ---------------------------------------------------------------------------
#

ENDPOINT_STATE=$OK

if [[ "$DOWN_ENDPOINTS" -gt 0 ]]; then
    ENDPOINT_STATE=$CRIT
fi

printf '%s\n' \
    "${ENDPOINT_STATE} \"Portainer Endpoints\" total=${ENDPOINT_COUNT};;;0|up=${UP_ENDPOINTS};;;0|down=${DOWN_ENDPOINTS};;;0 ${UP_ENDPOINTS}/${ENDPOINT_COUNT} endpoints UP"

exit 0